-
Notifications
You must be signed in to change notification settings - Fork 244
Implement file logging with default xunit tests #798
Conversation
using Xunit.Abstractions; | ||
using Xunit.Sdk; | ||
|
||
namespace Microsoft.AspNetCore.Testing.xunit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to make this type public from testing instead of duplicating here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
Assert.NotNull(LoggerFactory); | ||
LoggerFactory.CreateLogger(nameof(AssemblyTestLogTests)).LogInformation("Hello world"); | ||
LoggerFactory.CreateLogger(nameof(AssemblyTestLogTests)).LogDebug("Debug Hello world"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: add more tests
<PropertyGroup> | ||
<Description>Helpers for writing tests that use Microsoft.Extensions.Logging. Contains null implementations of the abstractions that do nothing, as well as test implementations that are observable.</Description> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I needed to cross-target to enable test discovery on full framework.
'\\', // back slash | ||
'/', // forward slash | ||
'\x7F', // delete | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ToArray
{ | ||
if (InvalidFileChars.Contains(c)) | ||
{ | ||
var escapedChar = Convert.ToByte(c).ToString("x"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This expands string size a lot, maybe just do '_' ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did do that at one point but it made test names highly unreadable. Also converting to the same character for all invalid chars creates lots of collisions. With the test name shortening logic in place, I don't think this should be an issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm right now I expand illegal characters like /
=> -x2f
, maybe I can reduce the verbosity and just leave it as 2f
which is a bit harder to parse out but still retains most of the information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did do that at one point but it made test names highly unreadable
Hex is not very readable too.
With the test name shortening logic in place, I don't think this should be an issue.
Replacing a bunch of '_' with one would be a nice addition to shortening logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Poor choice of words, I meant file name collisions instead of readability. Even though we have collision resolution, I would prefer retaining as much information as possible of the original arguments used by default. I've shortened the escaping to just the hex.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you really prefer Testing_https:-x2f-x2flocalhost
over Testing_https:_localhost
? To me, the second one is much easier to figure out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For that case maybe, but this will create many collisions for kestrel tests, for example: https://github.com/aspnet/KestrelHttpServer/blob/dev/test/shared/HttpParsingData.cs#L192. Also I don't like coalescing multiple _
since that makes tests like https://github.com/aspnet/KestrelHttpServer/blob/dev/test/shared/HttpParsingData.cs#L254-L255 indistiguishable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@halter73 what would you prefer?
{ | ||
public class LoggedTestCaseRunner : XunitTestCaseRunner | ||
{ | ||
public LoggedTestCaseRunner(IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Linebreaks? This line spans 2 monitors
{ | ||
var testClass = base.CreateTestClass(); | ||
|
||
if (testClass is LoggedTest loggedTestClass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we do it for any class, so you're not forced to inherit and can just inject the constructor argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are not injecting it as a constructor argument. We are setting the property of the base class LoggedTest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless you are suggesting we change the design?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I always thought we are going argument injection, that's why we started with argument injection DI prototype.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found a way of removing the need to flow the ITestOutputHelper with constructors.
{ | ||
public class LoggedTestXunitTests : LoggedTest | ||
{ | ||
public LoggedTestXunitTests(ITestOutputHelper output) : base(output) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still have to pass ITestOutputHelper
around? Can ILoggerFactory be injected directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to pass it around since we resolve the ITestOutputHelper
from the list of constructor args. Though I think removing it would be a breaking change so we'll need to update all the places currently using LoggedTest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I don't think we can remove ITestOutputHelper. What if the user wants to initialize a different LoggerFactory by calling StartLog (to override test name for example)? That call needs the ITestOutputHelper.
Note: WIP adding and fixing tests. |
{ | ||
public class LoggedTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer | ||
{ | ||
private List<(Type Type, IXunitTestCaseDiscoverer Discoverer)> Discoverers { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dictionary
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, at one point, I was using a best match on the type which needed a list of discoverers for types from the most specific to the most general. Now that I'm using an exact match on the type and falling back to non-logged discoverers, this can totally be just a dictionary.
if (testClass is LoggedTest loggedTestClass) | ||
{ | ||
var classType = loggedTestClass.GetType(); | ||
var testOutputHelper = ConstructorArguments.Single(a => typeof(ITestOutputHelper).IsAssignableFrom(a.GetType())) as ITestOutputHelper; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory we could get the ITestOutputHelper
from the source (whatever that is) rather than extracting it from constructor arguments.
var classType = loggedTestClass.GetType(); | ||
var testOutputHelper = ConstructorArguments.Single(a => typeof(ITestOutputHelper).IsAssignableFrom(a.GetType())) as ITestOutputHelper; | ||
var logLevelAttribute = TestMethod.GetCustomAttribute(typeof(LogLevelAttribute)) as LogLevelAttribute; | ||
var testName = TestMethodArguments.Aggregate(TestMethod.Name, (a, b) => $"{a}_{(b ?? "null")}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string.Join('_', ...)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closest I can come up with is string.Join("_", TestMethod.Name, TestMethodArguments)
but that won't be able to mark null
object with "null"
string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From adding tests previously, I remember it being useful to distinguish between null strings and empty strings for example. That's why I special cased null here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for foldl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is foldl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hipster name for Aggregate.
Supporting https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Testing/ITestSink.cs would be interesting too, would make some kestrel tests even simpler |
Ooh, yeah, automatically register a TestLogger and TestSink. I like that. If it's simple, do it, if it's more than a couple minutes work, file a bug and let's get this in. |
.Union(new char[] { | ||
' ', // space | ||
'\\', // back slash | ||
'/', // forward slash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this already an InvalidFileNameChar on every platform? Maybe a whitelist would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so but no harm in duplicates. I prefer not using a whitelist since that probably takes more work to construct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's way less that could go wrong with a whitelist and a lot less to think about. How do you justify replacing the delete character but not the other control characters?
I think it would take much effort to only allow alphanumeric characters plus underscore and replace the reset with underscore.
var skipReason = testMethod.EvaluateSkipConditions(); | ||
return skipReason != null | ||
? new[] { new SkippedTestCase(skipReason, DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) } | ||
: base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the base class not call EvaluateSkipConditions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not LoggedTheoryDiscoverer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But isn't that what you're replacing? If this didn't need to call EvaluateSkipConditions before, why now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I'm replacing ConditionalTheoryDiscoverer. But this inherits from LoggedTheoryDiscoverer which doesn't call EvaluateSkipConditions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't ConditionalTheoryDiscoverer have it's own LoggedTheoryDiscoverer that does exactly this then? Why not use that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I misunderstood. Could you derive from ConditionalTheoryDiscoverer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then I'll still need to override these two methods to create LoggedTestCase
or LoggedTheoryTestCase
. I don't see the benefit here.
var classType = loggedTestClass.GetType(); | ||
var testOutputHelper = ConstructorArguments.Single(a => typeof(ITestOutputHelper).IsAssignableFrom(a.GetType())) as ITestOutputHelper; | ||
var logLevelAttribute = TestMethod.GetCustomAttribute(typeof(LogLevelAttribute)) as LogLevelAttribute; | ||
var testName = TestMethodArguments.Aggregate(TestMethod.Name, (a, b) => $"{a}_{(b ?? "null")}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for foldl
private static readonly int MaxPathLength = 245; | ||
private static char[] InvalidFileChars = new char[] | ||
{ | ||
'\"', '<', '>', '|', '\0', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a static blacklist by concatenating https://github.com/dotnet/coreclr/blob/f5ae6d14d9d49dbb9f4e97f605d2a5cceab04879/src/mscorlib/shared/System/IO/Path.Unix.cs#L13-L15 and https://github.com/dotnet/coreclr/blob/9ac41819ab05282798c498b373a5017b1c2c4e64/src/mscorlib/shared/System/IO/Path.Windows.cs#L12-L28
and added space and delete.
cc @halter73
🆙📅 |
Can we bring |
We can also add a method to
to
Makes it a bit shorter and simpler. |
|
||
foreach (var c in s) | ||
{ | ||
sb.Append(InvalidFileChars.Contains(c) ? Convert.ToByte(c).ToString("x") : $"{c}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now when we have space in the invalid character list, please, let's not use hex, a bunch of random lower case characters would make file names completely unreadable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I like keeping the invalid character list to a minimum, but I vote for replacing all the invalid characters with underscore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only append underscore if the previous character is not underscore. A_B
is better than A_____B
and might help long paths.
// None resolved so create a new one and retain a reference to it for initialization/uninitialization | ||
if (loggedTestClass.TestOutputHelper == null) | ||
{ | ||
loggedTestClass.TestOutputHelper = _output = new TestOutputHelper(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't you have to call
https://github.com/xunit/xunit/blob/bccfcccf26b2c63c90573fe1a17e6572882ef39c/src/xunit.execution/Sdk/Frameworks/Runners/XunitTestRunner.cs#L65
for TestOutputHelper
to work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of comments then LGTM, let's try to use it. |
e86704c
to
3e6b132
Compare
Addressed the feedback items. |
c9f2f08
to
1b8e402
Compare
- Add filename handling for assembly test log
1b8e402
to
81b11e8
Compare
🎉 |
This is needed for aspnet/KestrelHttpServer#2390:
The usage is:
LoggedTest
LoggedTest